package com.cobo.zettelkasten.core

import com.cobo.zettelkasten.utils.Logger
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.apache.commons.io.FileUtils
import org.apache.commons.io.filefilter.FileFilterUtils
import org.apache.commons.io.filefilter.SuffixFileFilter
import org.apache.commons.lang3.StringUtils
import java.io.File


/**
 * @categories 分类
 * @tags 标签
 * @globalId 全局唯一id,用于规则化生成文章的访问路径，使得每次生成的路径保持唯一性
 * @filepath 源文件存储路径
 * @modifyTime 最后修改时间
 * @doubleLinks 双向链接关联文章,todo 需要加入tf-idf权重运算吗?
 * */
data class ZettelkastenMeta(
    var categories: Set<String> = HashSet(),
    var tags: Set<String> = HashSet(),
    var globalId: String,
    var filepath: String,
    var modifyTime: Long,
    var doubleLinks: Set<String> = HashSet()
)


/**
 * 默认使用 GrayMatterExtract 提取文本到项目
 * */
class Project(
    val projectDir: String,
    val author: String = "cobo",
    val doubleLinkStrategy: MultiBlogStrategy = NextjsDoubleLinkStrategy(),
    val storage: Storage = LocalFileStorage(projectDir + "/build"),
    val textExtract: RichTextExtract = GrayMatterExtract()
) {

    private val zettelkastenMetaPath = ".zettelkasten"
    private val gson = Gson()
    private val snowFlakeIdGen = SnowFlakeIdGenerator()


    /**
     * 查找编译项目，查询是否有上次编译结果缓存，已存在的文章尽量保持路径一致
     * */
    private val cacheMetadataMap: Map<String, ZettelkastenMeta> by lazy {
        val file = File(projectDir, zettelkastenMetaPath)
        val cacheMetadataList: ArrayList<ZettelkastenMeta>
        if (file.exists()) {
            val content = FileUtils.readFileToString(file, "utf-8")
            cacheMetadataList = gson.fromJson(content, object : TypeToken<ArrayList<ZettelkastenMeta>>() {}.type)
        } else {
            cacheMetadataList = java.util.ArrayList(0)
        }
        cacheMetadataList.associateBy { it.filepath }
    }

    /**
     * 此次编译生成的元数据描述
     * */
    val metadatas: ArrayList<ZettelkastenMeta> = ArrayList()

    fun generateGlobalId(): String {
        return snowFlakeIdGen.nextSnowFlakeId()
    }

    /**
     * 1. 不检查build和他下面的目录
     * 2. 无视所有 . 开头的文件
     * */
    private fun listMarkdownFiles(): MutableCollection<File>? {
        // 创建文件过滤器：只包含普通md文件，排除.所有.开头的文件
        val fileFilter = FileFilterUtils.and(
            FileFilterUtils.fileFileFilter(),
            SuffixFileFilter("md"),//
            FileFilterUtils.notFileFilter(
                FileFilterUtils.prefixFileFilter(".")
            )
        )

        // 创建目录过滤器：排除build目录
        val dirFilter = FileFilterUtils.notFileFilter(
            FileFilterUtils.nameFileFilter("build") // 排除build目录
        )
        return FileUtils.listFiles(File(projectDir), fileFilter, dirFilter)
    }


    // File -> RichText -> Markdown
    fun mapRichTextToMd(file: File, richText: RichText): Markdown {
        Logger.debug("编译文件 ${file.absolutePath}")
        val ctx = Context(file.absolutePath, projectDir)

        /**
         * 1. 双向链接解析
         * 2. formatter提取
         * 3. 用户提取
         * 4. 本地图片提取
         * 5. 标题抽取
         * */
        val advanceFactory = MarkdownAdvanceFactory(ctx)
        val processor = advanceFactory.commonMarkdown()
            .decorateWith(advanceFactory.localImageBase64EmbedDecorator())
            .decorateWith(advanceFactory.titleFormatterDecorator())
            .decorateWith(advanceFactory.tagsFormatterDecorator())
            .decorateWith(advanceFactory.categoryFormatterDecorator())
            .decorateWith(advanceFactory.baseInfoFormatterDecorator(author))
            .decorateWith(advanceFactory.doubleLinkDecorator())

        val md = processor.process(richText)
        // 查看缓存文件是否有，有则共享globalId 没有就生成新的雪花id
        md.globalId = cacheMetadataMap[md.filepath]?.globalId ?: generateGlobalId()
        return md
    }

    /**
     * markdown 转为 ZettelkastenMeta
     * */
    // Markdown -> ZettelkastenMeta
    fun mapMdToZettelkastenMeta(markdown: Markdown): ZettelkastenMeta {
        return ZettelkastenMeta(
            markdown.categories,
            markdown.tags,
            markdown.globalId,
            markdown.filepath,
            File(markdown.filepath).lastModified(),
            markdown.doubleLink
        )
    }

    //

    fun compile() {
        val markdownFiles = listMarkdownFiles()
        //[File] -> {filepath : Markdown}
        val filenameMdMap =
            markdownFiles?.map { mapRichTextToMd(it, textExtract.exec(FileUtils.readFileToString(it, "utf-8"))) }
                ?.associateBy { File(it.filepath).nameWithoutExtension }
        // 关联博客之间的双向链接
        doubleLinkStrategy.exec(filenameMdMap)

        //删除原有存储
        storage.clean()
        filenameMdMap?.values?.let { storage.exec(it) }

        // 缓存数据写入工程
        val zmetas = filenameMdMap?.values
            ?.map { mapMdToZettelkastenMeta(it) }?.toList() ?: ArrayList()
        FileUtils.writeStringToFile(File(projectDir, zettelkastenMetaPath), gson.toJson(zmetas), "utf-8")
    }

}

interface Storage {
    fun exec(markdowns: Collection<Markdown>)

    fun clean()
}

class LocalFileStorage(val filepath: String) : Storage {

    fun mapToText(markdown: Markdown): String {
        val textTpl = """
            |---
            |${markdown.formatter.entries.joinToString(separator = "\n") { (key, value) -> "$key: $value" }}
            |filepath = ${markdown.filepath}
            |globalId = ${markdown.globalId}
            |tags = ${markdown.tags.reduceOrNull { acc, s -> "$acc,$s" } ?: ""} 
            |categories = ${markdown.categories.reduceOrNull { acc, s -> "$acc,$s" } ?: ""}
            |doubleLinks = ${markdown.doubleLink.reduceOrNull { acc, s -> "$acc,$s" } ?: ""}
            |---
            |
            |${markdown.markdown}
        """.trimMargin("|")
        return textTpl
    }

    override fun exec(markdowns: Collection<Markdown>) {

        markdowns.forEach {
            val writeText = mapToText(it)
            FileUtils.writeStringToFile(File(filepath, "${it.globalId}.md"), writeText, "utf-8")
            Logger.debug("[${it.filepath}] 写入完成！globalId[${it.globalId}]")
        }
    }

    override fun clean() {
        //删除原有的编译好的文件夹
        FileUtils.deleteDirectory(File(filepath))
        //生成空的build目录
        FileUtils.forceMkdir(File(filepath))
    }
}

/**
 * todo 处理后数据存入minio
 * */
class MinioStorage(): Storage{
    override fun exec(markdowns: Collection<Markdown>) {
        TODO("Not yet implemented")
    }

    override fun clean() {
        TODO("Not yet implemented")
    }

}

/**
 * 多个博客关联策略
 * */
interface MultiBlogStrategy {
    fun exec(filenameMdMap: Map<String, Markdown>?)
}

class NextjsDoubleLinkStrategy : MultiBlogStrategy {
    /**
     * a 关联 b的时候，b也要关联到a，优化doublelink算法
     * 1. 自动主题提取
     * */
    override fun exec(filenameMdMap: Map<String, Markdown>?) {
        // 双向链接替换为标准的globalId 路径
        filenameMdMap?.forEach { (linkName, u) ->
            for (link in u.doubleLink) {
                if (filenameMdMap[link] != null) {
                    u.markdown =
                        StringUtils.replace(u.markdown, "[[$link]]", "[$link](/blogs/${filenameMdMap[link]?.globalId})")

                    //关联对象创建反向关联回来
                    (filenameMdMap[link]?.doubleLink as MutableSet<String>).add(linkName)
                }
            }
        }
    }
}
